#####################################################################
# File:         Model.py
# Product:      KnowledgeEngine
#
# Copyright:    Copyright (c) 2002-2004, Faulkner Technologies.
# Author:       Brian Oliver
#
# Description:  This file defines the Model class, the container for
#               all Component instances for performing assessments
#               and capturing knowledge
#
# See Also:     GenericObject, GenericContainer, 
#               Component, ComponentContainer
#####################################################################

#####################################################################
## Knowledge Engine Library Imports
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.KnowledgeEngineCore import *
from Products.KnowledgeEngine.IndexedGenericContainer import IndexedGenericContainer, IndexedGenericContainerInfo
from Products.KnowledgeEngine.Environment.Environment import Environment
from Products.KnowledgeEngine.AssessmentCache import AssessmentCache
from Products.KnowledgeEngine.Messaging.Message import Message
from Products.KnowledgeEngine.PropertyDefinition import *
from Products.KnowledgeEngine.PropertyTypes import *
from Products.KnowledgeEngine.PropertyFormats import *
from Products.KnowledgeEngine.PropertyStructures import *
from Products.KnowledgeEngine.EventDefinition import *
from Products.KnowledgeEngine.ReadableScore import ReadableScoreInfo, ReadableScore

#####################################################################
## Zope Library Imports
from Globals import DTMLFile
from Globals import Persistent
from ImageFile import ImageFile
from Products.ZSQLMethods.SQL import SQL

#####################################################################
## Python Library Imports
# (none)

#####################################################################
## ModelInfo Class Definition
class ModelInfo(IndexedGenericContainerInfo, ReadableScoreInfo):
    """
        Defines the class info methods, defining information about
        the class without needing an instance of the class
    """
    
    #---------------------------------------------------------------#
    
    def getClassName(self):
        """
            Returns the name of the class for which 
            we are providing information.
        """
        return 'Model'

    #---------------------------------------------------------------#

    def getDisplayName(self):
        """
            Return a friendly name for the class instances
            (this is used for displaying in lists etc)
        """
    
        return "Model"
        
    #---------------------------------------------------------------#

    def getIdentityType(self):
        """
            Return the identity type.  
        """
        
        return "model"
        
    #---------------------------------------------------------------#
    
    def getClassIcon(self):
        """
            Return the appropriate icon file for the class.
        """
        
        return ImageFile('www/icon.gif', globals())

    #---------------------------------------------------------------#

    def isInstanceOf(self, className):
        """
            Returns if the class is an instance of the specified
            className, or implements the interface specified 
            by the className
        """

        return IndexedGenericContainerInfo.isInstanceOf(self, className) or ReadableScoreInfo.isInstanceOf(self, className)
        
    #---------------------------------------------------------------#

    def isPermittedInContainer(self, containerClassInfo, containerInstance = None, instance=None):
        """
            ABSTRACT: Returns if class instances (instance) is permitted in 
            the specified container (containerInstance)
            
            NOTE: This is from this class' instance perspective.
        """
        
        #DEFAULT RULE: Models are only permitted in Environments or Folders
        return containerClassInfo.isInstanceOf('Environment') or containerClassInfo.isInstanceOf('Folder')

    #---------------------------------------------------------------#

    def isChildPermitted(self, childClassInfo, childInstance = None):
        """
            Returns if the specified child is permitted as a 
            child of the class' instances.
            
            NOTE: This is from this class' instance perspective.
            
            For component containers, this always overrides what the
            return value from childClassInfo.isPermittedInContainer(...)
        """
        
        #DEFAULT RULE: Models may only contain Sections and Reports
        return childClassInfo.isInstanceOf('Section') or childClassInfo.isInstanceOf('Report') or childClassInfo.isInstanceOf('Element') or childClassInfo.isInstanceOf('Event')
    
    #---------------------------------------------------------------#

    def onAfterEnvironmentInstanciated(self, environment):
        """
            Automatically called after an Environment has been 
            instanciated (either newly created or loaded from store).
            
            This method is useful for performing class specific 
            backwards/compatibility operations once an Environment 
            has been established.
        """
        
        #perform default processing
        IndexedGenericContainerInfo.onAfterEnvironmentInstanciated(self, environment)
        
        #create the model group of message topics/filters
        #environment.getMessaging().createGroup("models", ["model.added", "model.deleted"])
    
    #---------------------------------------------------------------#

    def onConfigureProperties(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register 
            class specific properties using the PropertyReflection interface.
            
            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered
            
            NOTE: This method is called before ClassInfo.onAfterStartup()            
        """

        #initialise the super-class properties for reflection
        IndexedGenericContainerInfo.onConfigureProperties(self)
        ReadableScoreInfo.onConfigureProperties(self)
        
        #add the 'author' property        
        self.addPropertyDefinition(PropertyDefinition(identity="author", displayName="Author", description="The name of the Model author", defaultValue="", propertyType=pt_STRING, propertyFormat=pf_TEXT_SINGLELINE, propertyStructure=ps_ATOMIC))        
        
        #add the 'description' property        
        self.addPropertyDefinition(PropertyDefinition(identity="description", displayName="Description", description="The description of the environment", defaultValue="", propertyType=pt_STRING, propertyFormat=pf_TEXT_MULTILINE, propertyStructure=ps_ATOMIC))        
    
        #add the 'passScore' property        
        self.addPropertyDefinition(PropertyDefinition(identity="passScore", displayName="Pass Score", description="The score that determines a pass or fail result - a negative value turns feature off", defaultValue="-1.0", propertyType=pt_FLOAT, propertyFormat=pf_NATIVE, propertyStructure=ps_ATOMIC))        
        
    #---------------------------------------------------------------#

    def onConfigureEvents(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register 
            class specific events using the EventReflection interface.
            
            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered
            
            NOTE: This method is called before ClassInfo.onAfterStartup()            
        """
                
        #initialise the super-class events for reflection
        IndexedGenericContainerInfo.onConfigureEvents(self)
        ReadableScoreInfo.onConfigureEvents(self)

        #add the onBeforeCloseAssessment event (which occurs prior to an assessment being closed)
        self.addEventDefinition(EventDefinition(identity="onBeforeCloseAssessment", displayName="Before Closing an Assessment", description="Occurs before an assessment is closed", eventType=et_SERVER))

        #add the onBeforeDeleteAssessment event (which occurs prior to an assessment being deleted)
        self.addEventDefinition(EventDefinition(identity="onBeforeDeleteAssessment", displayName="Before Deleting an Assessment", description="Occurs before an assessment is deleted", eventType=et_SERVER))

        #add the onBeforeLockAssessment event (which occurs prior to an assessment being locked)
        self.addEventDefinition(EventDefinition(identity="onBeforeLockAssessment", displayName="Before Locking an Assessment", description="Occurs before an assessment is locked", eventType=et_SERVER))

        #add the onBeforeUnlockAssessment event (which occurs prior to an assessment being unlocked)
        self.addEventDefinition(EventDefinition(identity="onBeforeUnlockAssessment", displayName="Before Unlocking an Assessment", description="Occurs before an assessment is unlocked", eventType=et_SERVER))

        #add the onBeforeSaveAssessment event (which occurs prior to an assessment being saved)
        self.addEventDefinition(EventDefinition(identity="onBeforeSaveAssessment", displayName="Before Saving an Assessment", description="Occurs before an assessment is saved", eventType=et_SERVER))

    #---------------------------------------------------------------#

#####################################################################
## Model Class Definition
class Model(IndexedGenericContainer, ReadableScore):
    """
        Base container for all Faulkner KnowledgeEngine Components
    """

    #---------------------------------------------------------------#

    #ABSTRACT TRANSIENT: a ClassInfo implementation instance to provide info about this class
    _CLASSINFO = ModelInfo()
 
    #---------------------------------------------------------------#

    # DTML Methods
    index_html = DTMLFile('dtml/index_html', globals())
    process_html = DTMLFile('dtml/process_html', globals())     #performs assessment storage/navigation
    
    #---------------------------------------------------------------#
    # Default Attribute Values

    #a dictionary of volatile references to components with in the Model
    #indexed by component Identity
    _v_Components = {}

    #---------------------------------------------------------------#

    def __init__(self, id, identity):
        "Constructor for the object"        
        
        IndexedGenericContainer.__init__(self, id, identity)
        
        #perform onAfterInstanciation
        self.onAfterInstanciation()

    #---------------------------------------------------------------#

    def onAfterInstanciation(self):
        """
            Handle Knowledge Engine specific actions after this object 
            has been loaded into memory.  
            
            This is useful for performing backwards compatibility
        """
        #set the volatile attributes
        self._v_Components = {}

        #perform default onAfterInstanciation for the container
        IndexedGenericContainer.onAfterInstanciation(self)

        #add passScore attribute if it doesn't already exist
        if not hasattr(self, "passScore"):
            self.passScore = "-1"
                
    #---------------------------------------------------------------#
        
    def getDescription(self):
        "Returns the Description of the model"
        return self.description        

    #---------------------------------------------------------------#
        
    def setDescription(self, description):
        "Sets the Description of the model"
        self.description = description

    #---------------------------------------------------------------#
        
    def getAuthor(self):
        "Returns the Author of the model"
        return self.author  

    #---------------------------------------------------------------#
        
    def setAuthor(self, author):
        "Sets the Author of the model"
        self.author = author

    #---------------------------------------------------------------#

    def getPassScore(self):
        "Returns the Pass Score of the model"
        return self.passScore  

    #---------------------------------------------------------------#
        
    def setPassScore(self, passScore):
        "Sets the Pass Score of the model"
        self.passScore = passScore

    #---------------------------------------------------------------#

    def getEnvironment(self):
        "Return the KnowledgeEngine Environment in which this Model is located"
        
        lParent = self.getParent()
        
        while not isinstance(lParent, Environment):
            lParent = lParent.getParent()
        
        return lParent

    #---------------------------------------------------------------#

    def getDatabaseConnection(self):
        """
            Return Connection object for the environment database.
        """

        #get and return a real connection from the environment
        return self.getEnvironment().getDatabaseConnection()

    #---------------------------------------------------------------#

    def onAfterAdd(self):
        """
            Handle Knowledge Engine specific actions after this Model
            has been added to an environment.
        """

        #perform normal onAfterAdd processing
        IndexedGenericContainer.onAfterAdd(self)

        #notify the environment that the model has been added
        try:
            self.getEnvironment().onAfterAddModel(self)
       
            #publish the "model.added" message
            self.publish(Message("model.added", modelIdentity=self.getIdentity()))
            
        except:
            pass
        
        ###print "DEBUG: Model.onAfterAdd: Completed"

    #---------------------------------------------------------------#

    def onBeforeIdentityChange(self):
        """
            Handle Knowledge Engine specific actions before the 
            identity of this object is changed.
            
            Called By GenericObject.onAfterClone
        """

        #perform normal onBeforeIdentityChange processing
        IndexedGenericContainer.onBeforeIdentityChange(self)

        try:
            ## The following code prevents duplicate Model cache entries 
            ## being created in an Environment when a Model is 
            ## copied between Environments.  That is, one model
            ## with two different Identities in the target Environment.
            ##
            ## The problem is caused by the Zope platform performing a copy,
            ## then an add before the clone callback is called.  
            ## The process is as follows; 
            ## 1. copy is created (without changing Identities... no problem)
            ## 2. copy is added into the target environment, which is different
            ##    from the environment being copied from.  Because the model
            ##    doesn't exist in the new environment, the Model is
            ##    added to the cache, with the original identity!
            ## 3. the onAfterClone callback is performed, which allocates 
            ##    new identities to the copies.  The model is then added to 
            ##    the target environment cache (again), but this time with a 
            ##    new identity... there you go, one model, two entries in an 
            ##    environment cache.
            
            #get the environment for this model
            environment = self.getEnvironment()
            
            #get the model with this model's identity from the environment
            model = environment.getModel(self.getIdentity())
            
            #is the model with this model's identity, this model?
            #(if it is, this model is already in the environment, so remove it)
            if self == model:
                #remove the model, as it will be added with a new identity
                #by the Model.onAfterClone
                environment.onBeforeDeleteModel(self)

        except:
            pass        
    
    #---------------------------------------------------------------#

    def onAfterClone(self, identityMappings = {}):
        "Handle Knowledge Engine specific actions after this object has been cloned"

        #perform normal onAfterAdd processing
        IndexedGenericContainer.onAfterClone(self, identityMappings)
        
        #clear the internal components dictionary (force reindexing)
        self._v_Components = {}

        #notify the environment that the model has been cloned (added)
        try:
            self.getEnvironment().onAfterAddModel(self)
        except:
            pass
        
        ###print "DEBUG: Model.onAfterClone: Completed"        

    #---------------------------------------------------------------#

    def onBeforeDelete(self):
        """
            Handle Knowledge Engine specific actions before this Model is deleted
        """
    
    #-----------------#
    #The following code has been commented out to preserve assessment data when a model is deleted.  
        ###remove component assessment data for this model
        ##for classInfo in getRegisteredClassInfo():
        ##    #is the class assessible?
        ##    if classInfo.isInstanceOf('Assessible'):
        ##        classInfo.removeState(self)
    ##
        ###remove the assessments for the model
        ##sql = "DELETE FROM assessments WHERE modelid='%s'" % (self.getIdentity(),)
        ##self.getSQLQuery()(query=sql)        
    #-----------------#

        #perform normal onBeforeDelete processing
        IndexedGenericContainer.onBeforeDelete(self)

        #notify the environment that the Model is about to be deleted
        try:
            self.getEnvironment().onBeforeDeleteModel(self)

            #publish the "model.deleted" message
            self.publish(Message("model.deleted", modelIdentity=self.getIdentity()))
        except:
            pass
        
        ###print "DEBUG: Model.onBeforeDelete: Completed"

    #---------------------------------------------------------------#
    
    def rebuildComponentsDictionary(self):
        """
            Attempt to rebuild the components dictionary, filling
            it with the components currently in the Model.
            
            Note: the component dictionary is volitile, so we have
            to rebuild it when the model is loaded/imported/copied.
        """
        
        ###print "DEBUG: Model.rebuildComponentsDictionary: Commenced"

        #PHASE 1: get a dictionary of all component descendants
        self._v_Components.clear()

        #get the sections in the model
        sections = self.getSections()

        #add each component to the components dictionary
        for section in sections:
            #add the section
            self._v_Components.update({section.getIdentity():section})

            #add the descendants of the section
            self._v_Components.update(section.getDescendantsInContext())
        ###print "DEBUG: Model.rebuildComponentsDictionary: Descendant Components Discovered"

        ###for identity in self._v_Components.keys():
        ###    print "DEBUG: Model.rebuildComponentsDictionary: Dump %s" % (identity,)

        #PHASE 2: resolve dependants collection for each component
        for component in self._v_Components.values():
            component.registerDependencies(None)
        ###print "DEBUG: Model.rebuildComponentsDictionary: Registered Component Dependencies"
    
        ###print "DEBUG: Model.rebuildComponentsDictionary: Completed for %d Components" % len(self._v_Components)
    
    #---------------------------------------------------------------#

    def onAfterAddComponent(self, component, componentContext):
        """
            Handle when a component has beed added somewhere in the Model. 
            Called by Component.onAfterAdd.
        """
        
        ###print "DEBUG: Model.onAfterAddComponent: Commenced"
        
        #does the dictionary have any components?
        if len(self._v_Components) == 0:
            self.rebuildComponentsDictionary()
            
        #add the component
        #(don't override previous entries with the same identity
        # so Cloning/Cut/Copy/Paste doesn't destroy existing entries)
        if not self._v_Components.has_key(component.getIdentity(componentContext)):
            self._v_Components[component.getIdentity(componentContext)] = component

        ###print "DEBUG: Model.onAfterAddComponent: Completed (%d)" % len(self._v_Components)

    #---------------------------------------------------------------#

    def onBeforeDeleteComponent(self, component, componentContext):
        """
            Handle when a component is about to be deleted from 
            somewhere with in the Model. 
            Called by Component.onBeforeDelete.
        """

        ###print "DEBUG: Model.onBeforeDeleteComponent: Commenced"

        #remove the specified component
        if self._v_Components.has_key(component.getIdentity(componentContext)):
            del self._v_Components[component.getIdentity(componentContext)]

        ###print "DEBUG: Model.onBeforeDeleteComponent: Completed (%d)" % len(self._v_Components)

    #---------------------------------------------------------------#

    def containsComponent(self, identity):
        """
            Returns if the component with the specified identity
            is in the model component dictionary
        """
        
        #does the dictionary have any components?
        if len(self._v_Components) == 0:
            self.rebuildComponentsDictionary()

        return self._v_Components.has_key(identity)

    #---------------------------------------------------------------#

    def getComponent(self, identity, forceLookup = false):
        """
            Return the component with the specified identity
            that exists within the Model.  
            
            If the component isn't found and forceLookup is true,
            the component dictionary is rebuilt and the lookup is 
            attempted again.
            
            Returns None if a component with the specified identity 
            does not exist in the model.
        """
        
        #does the dictionary have any components?
        if len(self._v_Components) == 0:
            self.rebuildComponentsDictionary()

        #attempt to locate the specified component
        if self._v_Components.has_key(identity):
            return self._v_Components[identity]
            
        #force a full lookup/search for the component        
        elif forceLookup:        
            #rebuild the component lookup cache
            self.rebuildComponentsDictionary()
            
            if self._v_Components.has_key(identity):
                return self._v_Components[identity]
            else:
                return None
            
        else:
            return None

    #---------------------------------------------------------------#

    def getSections(self):
        """
            Returns a sequence of the immediate child Sections(s)
            in this container, sorted by the Component.getOrder()
        """

        #get the immediate children of the container
        children = self.objectValues()

        #filter the Section(s) only
        sections = [x for x in children if x.isInstanceOf("ComponentContainer")]
        
        #sort the component by Component.getOrder()
        sections.sort(lambda x, y : -(x.getOrder() < y.getOrder()))

        #ensure that the sections have unique positions
        if self.isOrderedContainer():
            lastChild = None
            for child in sections:
                if lastChild == None:
                    lastChild = child
                else:
                    isAscending = lastChild.getOrder() < child.getOrder()
                    
                    if not isAscending:
                        child.setOrder(lastChild.getOrder() + 1)
                        
                    lastChild = child

        return sections                    

    #---------------------------------------------------------------#

    def getPages(self, assessment = None):
        """
            Returns a sequence of child Components (that may or not
            be ComponentContainers) that are considered top-level
            pages.  Takes into account the 'promotion' of children
            to be top-level pages, including nested promotions.
                        
            If an assessment is not provided, all possible 
            pages must be returned.            
        """

        #get all of the sections
        componentStack = self.getSections()
        
        #the resulting pages
        pages = []
        
        #go through the componentStack, and promote the appropriate children
        while len(componentStack) > 0:
        
            #pop the first component
            component = componentStack[0]
            del componentStack[0]
                        
            #is the component a section?
            if component.isInstanceOf("Section"):
            
                #is the section promoting it's children
                if component.getIsPromotingChildren():
                    #push the promoted children onto the front of the components stack
                    componentStack = component.getPromotedChildren(assessment) + componentStack
                else:
                    #a normal section... so it's a page
                    pages = pages + [component]
                    
            else:
                #not a section... so add the component as a page
                pages = pages + [component]
            
        return pages

    #---------------------------------------------------------------#

    def getComponentContainers(self):
        """
            Returns a sequence of immediate component containers (like sections)
            in the model, sorted by the Component.getOrder()
        """

        return self.getSections()

    #---------------------------------------------------------------#

    def getReports(self, type=None):
        """
            Returns a sequence of the immediate child Report(s)
            in this container, sorted by title. If type is specified
            then the results are filtered.
        """

        #get the immediate children of the container
        children = self.objectValues()

        #filter the Report(s) only
        reports = [x for x in children if x.isInstanceOf("Report")]
        
        if type != None:
            reports = [x for x in reports if x.getReportType() == type]
        
        #sort the report by Report.getTitle()
        reports.sort(lambda x, y : x.getTitle() < y.getTitle())

        return reports                    

    #---------------------------------------------------------------#

    def hasReportofType(self, type=None, identityType=None):
        """
            Returns whether or not Model contains any reports of a certain
            type (e.g. single, grouped) and IdentityType (e.g. simplereport,
            scoredreport etc...)
        """

        #get all reports of type
        reports = self.getReports(type)

        #filter out all reports of identityType
        reports = [x for x in reports if x.getIdentityType() == identityType ]
        
        if len(reports) > 0:
            return 1
        return 0             
  
    #---------------------------------------------------------------#
    
    def newAssessment(self, isAnonymous=false):
        """
            Create and return a new assessment cache with a 
            unique assessment identity.
        """

        if isAnonymous:
            assessmentCache = AssessmentCache(self, generateIdentity("anonaid"))
        else:
            assessmentCache = AssessmentCache(self, generateIdentity("aid"))
            
        #set that the assessment cache is new
        assessmentCache.setIsNew()
        
        return assessmentCache

    #---------------------------------------------------------------#

    def getAssessment(self, identity):
        """
            Return an AssessmentCache for the specified assessment
            identity.
        """
            
        return AssessmentCache(self, identity)
    
    #---------------------------------------------------------------#

    def getAssessmentIds(self):
        """
            Return the Assessment IDs of all the assessments for this model
            
            I cant decide how to do this as the assessments are not stored
            anywhere in particular - the UAM does have reference to model/assessments
            but it is on a per user basis. It may be necessary to query the DB
            and find all unique relationships between Models and Assessments.
        """

        assessments = []
        
        #First try the texts table
        sql = "SELECT DISTINCT assessmentid FROM texts WHERE modelid='%s'" % (self.getIdentity(),)
        (tcolumns, tresultset) = self.getDatabaseConnection().query(sql)

        for row in tresultset:
            assessments = assessments + [row[0]]

        #Then try the booleans table
        sql = "SELECT DISTINCT assessmentid FROM booleans WHERE modelid='%s'" % (self.getIdentity(),)
        (bcolumns,bresultset) = self.getDatabaseConnection().query(sql)
        
        for row in bresultset:
            if assessments.count(row[0]) == 0:
                assessments = assessments + [row[0]]
        
        return assessments        
        
            
    #---------------------------------------------------------------#

    def deleteAssessment(self, identity):
        """
            Deletes the specified assessment state from the system
        """

        #remove the assessment from the assessments table
        sql = "DELETE FROM assessments WHERE id='%d'" % (identity,)
        self.getDatabaseConnection().query(sql)
            
        #remove component assessment data for the assessment
        for classInfo in getRegisteredClassInfo():
            #is the class assessible?
            if classInfo.isInstanceOf('Assessible'):
                classInfo.deleteAssessment(self, identity)

        #publish the "assessment.deleted" message
        self.publish(Message("assessment.deleted", modelIdentity=self.getIdentity(), assessmentIdentity=identity))
        
    #---------------------------------------------------------------#

    def isOrderedContainer(self):
        """
            Return if the children of the object may use ordering.
            
            This is to allow the 'Order' column in the Content
            management tab in Zope            
        """

        return true
    
    #---------------------------------------------------------------#
        
    def getScore(self, assessmentCache, componentContext = None):
        """
            Return the score of the Model with the
            specified assessment cache.
        """

        #return the sum of the container scores that implement readablescore AND are shown given the specified assessment cache        
        result = 0
        for container in self.getComponentContainers():
            if container.isInstanceOf("ReadableScore") and container.isShown(assessmentCache, componentContext):
                result += container.getScore(assessmentCache, componentContext) 
                
        return result
        
    #---------------------------------------------------------------#
    
    def getMaxScore(self, assessmentCache, componentContext = None):
        """
            Return the Maximum total score of the Model with the
            specified assessment cache.
        """

        #return the sum of the container MaxScores that implement readablescore AND are shown given the specified assessment cache        
        result = 0
        for container in self.getComponentContainers():
            if container.isInstanceOf("ReadableScore") and container.isShown(assessmentCache, componentContext):
                result += container.getMaxScore(assessmentCache, componentContext) 
                
        return result
        
    #---------------------------------------------------------------#

#####################################################################
## Class Initialisation

# Register Class Information with the Knowledge Engine
registerGenericObjectClass(Model)

#####################################################################
